通常,对于 JavaScript 脚本而言,如果运行脚本时出现错误会挂掉,即引擎会在错误代码处停下来,不会继续执行后续代码,而是将错误信息打印到控制台。比如:
1 | let a = 1; |
上面的代码中,执行到 (*)
这一行便会停下来,将错误信息打印到控制台。
try…catch 结构
但是,有一种语法结构 try...catch
允许我们捕获错误并作出相应的处理,这样脚本在出现错误时不会挂掉,而是执行我们设定的错误处理代码。
我们来看两个例子:
1 | try { |
上面的代码中,因为 try
语句块没有错误,所以 catch
语句块内的代码会被忽略,不会执行。
1 | try { |
上面的代码中,由于 try
语句块内存在错误:变量未定义,所以 try
语句块内这一行之后的代码都不会执行,直接跳转到 catch
语句块内执行错误处理代码。
try…catch 只能捕获运行时错误
所谓运行时错误 runtime-error
,是指有效的 JavaScript 代码,即 JavaScript 引擎可以正确解析的代码。对于一个 JavaScript 脚本,引擎首先会解析它,接着执行它。如果出现解析时错误,通常是语法错误,引擎会直接报错,因为引擎这时无法读懂代码,自然地,try..catch
结构不可能捕获到解析错误。比如:
1 | try { |
try…catch 是同步执行的
在诸如定时器 setTimeout
等异步代码中发生错误,try...catch
结构无法捕获错误。比如:
1 | try { |
原因在于,setTimeout
的回调函数在执行时,引擎实际上已经离开了 try...catch
结构体。要捕获类似这样的错误,需要这样做:
1 | setTimeout(function() { |
错误对象
当发生运行时错误时,引擎会创建一个错误对象,里面包含了有关这次错误的信息。该错误对象会被当作参数传递给 catch
语句:
1 | try { |
错误对象主要有 2 个属性:
name
错误的名称,对于未定义的变量而言,是引用错误ReferenceError
。message
有关错误详情的文本信息。
还有一个非标准但是被广泛采用的属性:
stack
主要用作调试,包含了导致错误的调用栈跟踪。
实例
让我们来看一个实际的例子:解析从服务器获取的 JSON 数据。正常的情况下,应该是这样的:
1 | const json = '{"name":"John", "age": 30}'; // data from the server |
JSON 格式错误
但是实际情况往往复杂多变,首先考虑一种情况,假如 JSON 数据不合法(格式错误,无法被正确解析),那么脚本运行到解析 JSON 数据时将会直接挂掉。这显然不是我们想要的结果,这也会让用户非常困惑。我们可以使用 try...catch
来进行错误处理:
1 | const json = "{ bad json }"; |
抛出错误
再考虑另一种情况:JSON 格式是对的,但是不包含我们需要的字段,在这里是 name
字段:
1 | const json = '{ "age": 30 }'; // incomplete data |
对于这种情况,我们可以使用 throw
操作符来抛出错误:
1 | const json = '{ "age": 30 }'; // incomplete data |
重新抛出错误
接着考虑更复杂的情况,除了 JSON 数据字段缺失的错误,假如 try
语句块内还有其他的错误,比如未定义的变量,如何在 catch
语句块内处理这种情况?接着上面的例子:
1 | const json = '{ "age": 30 }'; // incomplete data |
上面代码标有 (*)
的一行有一个未定义的变量,于是引擎会创建错误对象并跳转到 catch
语句块。需要明确的一点是,catch
会从 try
中捕获所有的错误。对于类似上面的例子,解决思路很简单:catch
语句块应该只处理它知道的错误并重新抛出其他错误。
这一过程大致如下:
catch
会捕获try
内的所有错误。- 在
catch
语句块内,我们通过错误对象的name
属性来分析错误。 - 只处理我们知道如何处理的错误,重新抛出其他错误。
针对上面的提到的同时存在未定义变量错误和 JSON 语法错误,我们只需要处理 JSON 语法错误,而将其他错误重新抛出:
1 | const json = '{ "age": 30 }'; // incomplete data |
上面代码中,try...catch
只处理了它关心的 JSON 语法错误,而将其他错误重新抛出。那么其他错误最终到哪里去了呢?
两种可能:如果外部代码没有使用 try...catch
来捕获错误,那么会导致脚本挂掉;如果外部代码使用了 try...catch
结构,则会捕获重新抛出的错误。如下面代码所示:
1 | function readData() { |
上面的代码中,内层的 try...catch
只处理了语法错误,其他的错误都由外层的 try...catch
来处理。
注意事项
在
try...catch...finally
语句块内声明的变量只在该语句块没可见。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24let num = +prompt("Enter a positive integer number?", 35)
let diff, result; // 注意这里变量都声明在 try...catch...finally 语句块之外
function fib(n) {
if (n < 0 || Math.trunc(n) != n) {
throw new Error("Must not be negative, and also an integer.");
}
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
let start = Date.now();
try {
result = fib(num);
} catch (e) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "error occured");
alert( `execution took ${diff}ms` );finally
语句块总是会执行,即使try
语句块内有显式的返回:1
2
3
4
5
6
7
8
9
10
11
12
13function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( "finally" );
}
}
alert( func() ); // first works alert from finally, and then this one
全局捕获错误
有一个不属于语言规范,但是各大浏览器都实现了的全局捕获错误的回调函数 window.onerror。它的主要作用不是为了让脚本可以继续执行,而是通常用作错误报告,即将错误信息发送给开发者。在页面中插入下面的脚本,即可实现错误报告的效果:
1 | <script> |
定制和扩展错误
在实际开发中,语言内置的几个标准错误类,比如 Error
,SyntaxError
,TypeError
,ReferenceError
等,可能不足以满足我们在特定情况下的需要。比如在进行网络请求操作时我们可能需要 HttpError
,在进行数据库操作时我们可能需要 DbError
,对于搜索操作可能需要 NotFoundError
等。我们可以通过继承通用错误类 Error
来定制我们需要的错误类,这被认为是最佳实践。有以下优点:
- 可以继承
message
,name
,stack
这些基础的错误属性。 - 可以使用
inctanceof
运算符来判断错误类型。 - 便于之后的多级错误类型继承的形成。
当然,对于不同的错误类,我们可以添加额外所需的属性,比如对于 HttpError
,可以添加 statusCode
属性,它的值可能是 404
,500
等。
扩展错误实例
让我们来看一个读取 JSON 格式的用户数据的例子。假定我们期望的用户数据是这样的:
1 | const json = `{ "name": "John", "age": 30 }`; |
先做一点铺垫,内置的通用错误类 Error
的伪代码可能是这样的:
1 | // The "pseudocode" for the built-in Error class defined by JavaScript itself |
为了将 JSON 数据字段缺失的错误单独处理,我们定制一个单独的 ValidationError
错误类:
1 | class ValidationError extends Error { |
接着我们将它用在读取用户数据的例子上:
1 | class ValidationError extends Error { |
注意上面代码使用 instanceof
运算符来判断错误类型的做法。
进一步扩展错误类
上面的 ValidationError
错误类还是过于通用,我们在它的基础上继续扩展一个更具体的属性缺失错误类 PropertyRequireError
:
1 | class ValidationError extends Error { |
现在,我们在抛出属性错误的时候只需要传入缺失的属性就可以了。还有一个地方可以优化,每次扩展一个类都需要设置 this.name = ...
,可以增加一个继承的层级来专门完成这个任务:
1 | class MyError extends Error { |
包装异常
让我们思考一下,readUser
这个函数的任务是从 JSON 数据读取到我们所需要的用户数据字段。让我们站在 readUser
函数的调用者的角度来思考,我们希望得到的错误信息应该简单清晰,是一个类似 ReadError
这样的错误类。至于错误的具体细节应该封装在这个错误类内部,可能是 JSON 格式错误,可能是属性缺失错误,以及将来可能出现的其他错误。
1 | class MyError extends Error { |
上面代码所使用的方式叫做包装异常 Wrapping Exceptions,是一种在面向对象编程中广泛使用的技巧。